package mygame;

import com.jme3.export.InputCapsule;
import com.jme3.export.JmeExporter;
import com.jme3.export.JmeImporter;
import com.jme3.export.OutputCapsule;
import com.jme3.export.Savable;
import com.jme3.math.FastMath;
import com.jme3.math.Vector3f;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;

/** 
 * This class houses the voxel grid, which holds all voxels currently in the world.
 * It also contains static methods for mathematical operations on world locations and voxel grid locations.
 */
public class VoxelGrid implements Savable {
    /** The voxel grid is actually split into y-layers. */
    protected Map<Integer, VoxelLayer> voxelLayers = new HashMap<Integer, VoxelLayer>();
    protected static boolean debugging = true;
    
    /**
     * Adds a voxel to the voxel grid in the appropriate layer.
     * NOTE: This method does NOT check to see if a voxel already exists there. If
     * there is one, it will be overwritten and then who knows what would happen! Be careful!
     */
    public void addVoxel(Voxel voxel) {
	System.out.println("Adding voxel to grid at: "+voxel.getVoxelGridLocation());
	VoxelLayer layer = getVoxelLayer(voxel);
	if(layer == null) { //if layer doesn't exist yet, create it
	    layer = new VoxelLayer(getLayerNum(voxel));
	    addVoxelLayer(layer);
	}
	layer.addVoxel(voxel);
	if(debugging) {
	    DataStore.voxelWireframeNode.attachChild(voxel.getWireframe());
	}
    }
    
    public void removeVoxel(Voxel voxel) {
	getVoxelLayer(voxel).removeVoxel(voxel);
        if(debugging) {
	    DataStore.voxelWireframeNode.detachChild(voxel.getWireframe());
	}
    }
    
    public Voxel getVoxelAtLocation(Vector3i voxelGridLocation) {
        return getVoxelLayer(voxelGridLocation).getVoxelAtLocation(voxelGridLocation);
    }
    
    public boolean isVoxelAtLocation(Vector3i voxelGridLocation) {
	VoxelLayer layer = getVoxelLayer(voxelGridLocation);
	if(layer == null)
	    return false;
	else
	    return layer.isVoxelAtLocation(voxelGridLocation);
    }

    //helper methods dealing with layers

    protected void addVoxelLayer(VoxelLayer voxelLayer) {
	voxelLayers.put(voxelLayer.getLayerNum(), voxelLayer);
    }

    protected int getLayerNum(Voxel voxel) {
	return getLayerNum(voxel.getVoxelGridLocation());
    }

    protected int getLayerNum(Vector3i voxelGridLocation) {
	return voxelGridLocation.getY();
    }

    protected VoxelLayer getVoxelLayer(Voxel voxel) {
	return voxelLayers.get(getLayerNum(voxel.getVoxelGridLocation()));
    }

    protected VoxelLayer getVoxelLayer(Vector3i voxelGridLocation) {
	return voxelLayers.get(getLayerNum(voxelGridLocation));
    }

    public VoxelLayer getVoxelLayer(int layerNum) {
	return voxelLayers.get(layerNum);
    }

    //fun mathematical calculations

    public static Vector3i getVoxelGridLocation(Vector3f worldLocation) {
	//right now this only works if VOXEL_EXTENT > 1
	Vector3i voxelGridLocation = new Vector3i();
	float offsetX = Voxel.VOXEL_EXTENT*FastMath.sign(worldLocation.getX());
	float offsetY = Voxel.VOXEL_EXTENT*FastMath.sign(worldLocation.getY());
	float offsetZ = Voxel.VOXEL_EXTENT*FastMath.sign(worldLocation.getZ());
	voxelGridLocation.setX((int)((worldLocation.getX() + offsetX)/(Voxel.VOXEL_DIAMETER)));
	voxelGridLocation.setY((int)((worldLocation.getY() + offsetY)/(Voxel.VOXEL_DIAMETER)));
	voxelGridLocation.setZ((int)((worldLocation.getZ() + offsetZ)/(Voxel.VOXEL_DIAMETER)));
	return voxelGridLocation;
    }

    public static Vector3f getWorldLocation(Vector3i voxelGridLocation) {
	Vector3f worldLocation = new Vector3f(voxelGridLocation.getX(), voxelGridLocation.getY(), voxelGridLocation.getZ());
	worldLocation.multLocal(Voxel.VOXEL_DIAMETER);
	return worldLocation;
    }

    /**
     * Returns the voxel grid location of the voxel adjacent to the given one,
     * in the direction of the face closest to the given point.
     */
    public static Vector3i getLocationAdjVoxelFromPoint(Voxel voxel, Vector3f pointOnFace) {
	return getLocationAdjVoxel(voxel, getFaceClosestToPoint(voxel, pointOnFace));
    }

    /** Returns a string representing the voxel's face closest to the given point. */
    public static String getFaceClosestToPoint(Voxel voxel, Vector3f point) {
        //TODO this doesn't work right with points inside the voxel. revisit this later...
	String faceDirection;
	Vector3f diff = point.subtract(VoxelGrid.getWorldLocation(voxel.getVoxelGridLocation()));
	//the longest direction indicates the closest face
	if (Math.abs(diff.getX()) > Math.abs(diff.getY())) {
	    if (Math.abs(diff.getX()) > Math.abs(diff.getZ())) {
		if (FastMath.sign(diff.getX()) == 1)
		    faceDirection = "xPos";
		else
		    faceDirection = "xNeg";
	    } else {
		if (FastMath.sign(diff.getZ()) == 1)
		    faceDirection = "zPos";
		else
		    faceDirection = "zNeg";
	    }
	} else {
	    if (Math.abs(diff.getY()) > Math.abs(diff.getZ())) {
		if (FastMath.sign(diff.getY()) == 1)
		    faceDirection = "yPos";
		else
		    faceDirection = "yNeg";
	    } else {
		if (FastMath.sign(diff.getZ()) == 1)
		    faceDirection = "zPos";
		else
		    faceDirection = "zNeg";
	    }
	}
	return faceDirection;
    }

    /** Returns the voxel grid location of the voxel adjacent to the given one in the given direction. */
    public static Vector3i getLocationAdjVoxel(Voxel voxel, String direction) {
	Vector3i location = voxel.getVoxelGridLocation().clone();
	if (direction.equals("xPos")) {
	    location.setX(location.getX()+1);
	    return location;
	} else if (direction.equals("xNeg")) {
	    location.setX(location.getX()-1);
	    return location;
	} else if (direction.equals("yPos")) {
	    location.setY(location.getY()+1);
	    return location;
	} else if (direction.equals("yNeg")) {
	    location.setY(location.getY()-1);
	    return location;
	} else if (direction.equals("zPos")) {
	    location.setZ(location.getZ()+1);
	    return location;
	} else if (direction.equals("zNeg")) {
	    location.setZ(location.getZ()-1);
	    return location;
	} else {
	    //not sure what to do here on an invalid value
	    return null;
	}
    }

    public void write(JmeExporter ex) throws IOException {
	OutputCapsule capsule = ex.getCapsule(this);
	capsule.writeSavableArrayList(new ArrayList<VoxelLayer>(voxelLayers.values()), "voxelLayers", new ArrayList<VoxelLayer>());
    }

    @SuppressWarnings("unchecked")
    public void read(JmeImporter im) throws IOException {
	InputCapsule capsule = im.getCapsule(this);
	ArrayList<VoxelLayer> layers = capsule.readSavableArrayList("voxelLayers", new ArrayList<VoxelLayer>());
	for(VoxelLayer layer : layers) {
	    addVoxelLayer(layer);
	}
    }
}
